import ij.IJ;
import ij.ImagePlus;
import ij.gui.NewImage;
import ij.process.ImageProcessor;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Stack;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;

/**
*
* Bachelor thesis at the Institute of Biomedical Engineering, 
* located at the UMIT/EWZ campus in Hall in Tyrol. <br><br>
* 
* This class offers methods for cellular automaton segmentation in an 3 dimensional
* stack image. First of all, the image is analysed and the necessary parameters are
* calculated. After, the segmenttion starts, the result is get and visualized. The
* segemntation result can be saved as a binary image.
* 
* @author Stefan Fuchs
*
*/
public class CASegmentation3D implements Runnable, ActionListener,
		WindowListener {

	//////////////////////////////////////////////////////
	
	private int maxOutlier = 15;
	
	//////////////////////////////////////////////////////
	
	/** Calculates Moore Neighborhood */
	public static final int NEIGHBOR_MOORE = 0;

	/** Calculates von Neumann Neighborhood */
	public static final int NEIGHBOR_VON_NEUMANN = 1;

	/** Calculates global maximum greyvalue */
	public static final int MAXGREYVALUE_GLOBAL = 2;

	/** Calculates local maximum greyvalue */
	public static final int MAXGREYVALUE_LOCAL = 3;

	/** Calculates euclidean distance */
	public static final int DISTANCE_EUCLIDEAN = 4;

	/** Calculates manhattan distance */
	public static final int DISTANCE_MANHATTAN = 5;

	// int[]
	/** Array with all the grey value based voxel values of the image. */
	private int[] greyValue;

	/** Array with the labels of the voxels. */
	private int[] label;

	/** Array with the labels of the voxels at t+1. */
	private int[] labelT1;
	
	/** The labeled 1-D image*/
	private int[] connectedComponentsLabel;

	// int
	/** Height of the Image */
	private int height;

	/** Width of the Image */
	private int width;

	/** Amount of Slices in the stack */
	private int size;
	
	/** Min Roi-Prameters X-Axis */
	private int minRoiX;

	/** Max Roi-Prameters X-Axis */
	private int maxRoiX;

	/** Min Roi-Prameters Y-Axis */
	private int minRoiY;

	/** Max Roi-Prameters Y-Axis */
	private int maxRoiY;

	/** Min Roi-Prameters Z-Axis */
	private int minRoiZ;

	/** Max Roi-Prameters Z-Axis */
	private int maxRoiZ;

	/** Width from selected roi. */
	private int roiWidth;

	/** Height from selected roi. */
	private int roiHeight;

	/** Slices from selected roi. */
	private int roiSize;

	/** Slices from selected roi. */
	private int roiArea;

	/** Maximal grey value, can be local or global. */
	private int maxGreyValue;

	/** Number of segmentation steps. */
	private int steps;

	/**
	 * Number of steps after which the current status of the segmentation is
	 * visualized.
	 */
	private int tmpSteps;

	/** Quality factor for calculating the steps. */
	private int stepsFactor;

	/** Selected neighbor option. */
	private int neighborOption;

	/** Selected max. grey value option. */
	private int greyValueOption;

	/** Selected distance option. */
	private int distanceOption;

	/** Sequence counter for generating image titles. */
	private int seqCounter;

	// long
	/** Segmentation time left. */
	private long timeLeft;

	/** Duration of segmentation process. */
	private long segTime;

	/** Segmentation start time. */
	private long segStart;

	/** Segmentation stop time. */
	private long segStop;

	// double[]
	/** Array with the strength values of the pixels. */
	private double[] strength;

	/** Array with the strength values of the pixels at t+1. */
	private double[] strengthT1;

	// double
	/** Factor for the strength between the slices. */
	private double strengthFactor;

	// String
	/** Name of current image. */
	private String imageName;

	// boolean
	/** Indicator if segmentation is running or not. */
	private boolean running = false;

	// ArrayList

	/** List of all segmentated points. */
	private ArrayList<Voxel> segmentedPoints;
	
	/** List of segemented Points of Ventricle A*/
	private ArrayList<Voxel> ventriclePoints;
	
	// General Objects

	/** Singelton Object */
	private static CASegmentation3D instance = null;

	/** StringBuffer for the result text. */
	private StringBuffer resultText;

	/** Formates the time. */
	private SimpleDateFormat sdf;

	/** Formates the time. */
	private SimpleDateFormat sdfSD;

	/** Timer to update time left. */
	private Timer timer;

	/** Segmentation thread. */
	private Thread segThread;

	/** Status-Dialog to display segmentation progress */
	private StatusDialog sd;
	
	/** The Modell */
	private Modell modell;

	// /////////////////////////////
	// LET THE SHOW BEGIN :-) /////
	// /////////////////////////////

/**
	 * Private Constructor for the singleton
	 */
	private CASegmentation3D() {
		//dummy constructor
	}

	/**
	 * Prepear the Segmentation, create a new Thread
	 */
	public void prepearCaSegmentation() {
		this.running = true;
		this.segThread = new Thread(CASegmentation3D.getInstance());
		this.segThread.start();
	}

	/**
	 * Getter for the Singelton
	 * 
	 * @return The only instance of {@link CASegmentation3D}
	 */
	public static synchronized CASegmentation3D getInstance() {

		if (instance == null) {
			instance = new CASegmentation3D();
		}
		return instance;
	}

	/**
	 * Runable for Thread
	 */
	@Override
	public void run() {
		
		long start = System.currentTimeMillis();
		this.modell = Modell.getInstance();
		getDimensionParameters();
		
		this.sd = new StatusDialog();
		this.sd.getBtnAbort().addActionListener(this);
		this.sd.getJfDialog().addWindowListener(this);

		this.segTime = -System.currentTimeMillis();
		this.segStart = System.currentTimeMillis();

		this.resultText = new StringBuffer();
		this.steps = calcSteps();
		disassambleImage();
		
		maxOutlier *= (this.maxRoiZ - this.minRoiZ);
		//TODO:
		
		/*
		 * Get Segmentation Parameters for Log-file
		 */
		getSegmentationParameters();
		
		/*
		 * Start Segmentation
		 */
		startSegmentation();
		getSegmentationResult();
		visualizeSegmentation(" Seg. Result Points");
		
		this.sd.close();
		
		this.segTime = System.currentTimeMillis() + this.segTime;
		this.segStop = System.currentTimeMillis(); 
		
		addResult("\nSegmented points: " + this.segmentedPoints.size());
		TimeZone.setDefault(TimeZone.getTimeZone(System.getProperty("user.timezone")));
		this.sdf = new SimpleDateFormat("HH:mm:ss - dd.MM.yyyy");
		addResult("Segmentation finished..." + this.sdf.format(new Date()));
		TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
		this.sdf = new SimpleDateFormat("HH:mm:ss");
		addResult("Duration: " + this.sdf.format(this.segTime));
		
		writeResult(this.resultText.toString());
		
		System.out.println(this.segmentedPoints.size());
		System.out.println("Ende.");
		
		/*
		 * Clear memory
		 */
		this.modell.setConnectedComponentLabel(Util.copy1DVoxelArry(this.connectedComponentsLabel));
		this.running = false;
		
		long stop = System.currentTimeMillis();
		long time = stop - start;
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		System.out.println("Segmentation Duration: " + sdf.format(time));
		
		clearAll();
	}

	/**
	 * Calculates number of segmentation steps, depending on the roi area. <br><br>
	 * steps = (roi_area / steps_factor) + number_of_roi_slices
	 * 
	 * @return
	 * 			steps
	 */
	private int calcSteps() {
		this.steps = 0;
		if ( this.stepsFactor == 0 ) {
			return 0;
		}
		calcRoi();
		return (int) Math.round((this.roiArea / this.stepsFactor ) + ( 2* this.roiSize ));
		
	}

	/**
	 * Calculates ROI-Area
	 * 
	 */
	private void calcRoi() {
		this.roiWidth = this.maxRoiX - this.minRoiX + 1;
		this.roiHeight = this.maxRoiY - this.minRoiY + 1;
		this.roiSize = this.maxRoiZ - this.minRoiZ;
		this.roiArea = this.roiWidth * this.roiHeight;
	}

	/**
	 * Prepares the image and initializes the data structures for further
	 * segmentation commands.
	 */
	private void disassambleImage() {
		
		this.label = new int[this.width * this.height * this.size];
		this.labelT1 = new int[this.width * this.height * this.size];

		this.strength = new double[this.width * this.height * this.size];
		this.strengthT1 = new double[this.width * this.height * this.size];

		for(int z = 0; z < this.size; z ++){
			for(int y = 0; y < this.height; y++) {
				for(int x = 0; x < this.width; x++){
					int coordinate1D = (int)(this.width * (this.height * z + y) + x);
					
					/*
					 * Global grey value
					 */
					if(this.greyValueOption == CASegmentation3D.MAXGREYVALUE_GLOBAL 
							&& this.greyValue[coordinate1D] > this.maxGreyValue){
						this.maxGreyValue = this.greyValue[coordinate1D];
					}

					this.strength[coordinate1D] = 0.0d;
					this.label[coordinate1D] = 0;
				}
			}
		}
		
		for(Voxel v : this.modell.getInternalVoxels()){
			int coordinate1D = (int)(this.width * (this.height* v.getZ() + v.getY()) + v.getX());
			this.strength[coordinate1D] = 0.9d;
			this.label[coordinate1D] = Modell.INTERNAL_POINTS_LABEL.getRGB();
		}
		
		for(Voxel v : this.modell.getExternalVoxels()){
			int coordinate1D = (int) (this.width * (this.height * v.getZ() + v.getY()) + v.getX());
			this.strength[coordinate1D] = 0.9d;
			this.label[coordinate1D] = Modell.EXTERNAL_POINTS_LABEL.getRGB();
		}
	}
	
	/**
	 * Get segmentation parameter for logfile. <br><br>
	 */
	private void getSegmentationParameters(){
		TimeZone.setDefault(TimeZone.getTimeZone(System.getProperty("user.timezone")));
		this.sdf = new SimpleDateFormat("HH:mm:ss - dd.MM.yyyy");
		String separator = System.getProperty("line.separator");
		addResult("3D-Segmentation from image: " + this.imageName + separator + separator);
		addResult("Segmentation Parameters:");
		
		if(this.neighborOption == CASegmentation3D.NEIGHBOR_MOORE)
			addResult("Moore Neighborhood");
		else
			addResult("von Neumann Neighborhood");
		
		if(this.distanceOption == CASegmentation3D.DISTANCE_EUCLIDEAN)
			addResult("Euclidean Distance");
		else
			addResult("Manhattan Distance");
		
		if(this.greyValueOption == CASegmentation3D.MAXGREYVALUE_GLOBAL)
			addResult("Global grey value");
		else
			addResult("Local grey value");

		addResult(separator + "ROI Parameters:");
		addResult("X-Axis: " + this.minRoiX + " to " + this.maxRoiX);
		addResult("Y-Axis: " + this.minRoiY + " to " + this.maxRoiX);
		addResult("Z-Axis: " + (this.minRoiZ + 1) + " to " + this.maxRoiZ);
		addResult(separator + "ROI Area:");
		addResult("Width: " + this.roiWidth + " pixel");
		addResult("Height: " + this.roiHeight + " pixel");
		addResult("Size: " + this.roiSize + " slices");		
		addResult("Area: " + this.roiArea + " pixel²");
		addResult("Volume: " + (this.roiArea * this.roiSize) + " voxel");
		addResult(separator + "Strength factor between slices: " + this.strengthFactor);
		addResult("Quality factor: " + this.stepsFactor);
		addResult("Number of segmentation steps: " + this.steps);
		addResult(separator + separator + "Segmentation started..." + this.sdf.format(this.segStart));
	}
	
	/**
	 * Adds {@link text} to the complete result text.
	 * 
	 * @param 	text 
	 * 				Text strip
	 */
	private void addResult(String text){
		this.resultText.append(text + System.getProperty("line.separator"));
	}

	/**
	 * Start the segmentation function. 
	 */
	private void startSegmentation() {
		this.sd.setTitle("Segmentating");
		this.sd.setTime("Time left: --:--:--");
		this.sd.show(true);
		this.timer = new Timer();
		long start = System.currentTimeMillis();
	
		for(int stepsCounter = 0; stepsCounter <= this.steps; stepsCounter++){
			if(this.tmpSteps != 0 && stepsCounter % this.tmpSteps == 0){
				getSegmentationResult();
				visualizeSegmentation(" TEMPORARY (step = " + stepsCounter + ") Result");
			}
			if(stepsCounter == 1){
				calcTimeLeft(start);
				this.timer.schedule(new TimerUpdate(CASegmentation3D.getInstance()), 0, 1000);
			}
			else if(stepsCounter == this.steps - 1 && this.timer != null){
				this.timer.cancel();
				this.timer = null;
				System.gc();
			}
			
			//System.out.println("Step: " + stepsCounter);
			
			this.sd.setStatus("Segmentation done: " + (int)(100 * stepsCounter / this.steps) + " %");
			/*
			 * Crop segmentation area to roi
			 */
			for(int z = this.minRoiZ; z < this.maxRoiZ; z++){
				for(int y = this.minRoiY; y < this.maxRoiY; y++){
					for(int x = this.minRoiX; x < this.maxRoiX; x++){
						performSegmentationCellStep(x, y, z);
					}
				}
			}
			prepareNextStep();
		}
	}
	
	
	/**
	 * Visualize the segmentation result in a new image stack / sequence.
	 */
	private void visualizeSegmentation(String title){
		this.sd.setStatus("Visualizing Segmentation Result...");
		String counter = "";
		ImagePlus segmentedPointsImage = NewImage.createByteImage(this.imageName + title,this.width, this.height, this.size, NewImage.FILL_BLACK);
		ImagePlus segmentedLabeledPointsImage = new ImagePlus(this.imageName + title, this.modell.getImageStack());
		
    	ImageProcessor segmentedPointsImageProc = segmentedPointsImage.getProcessor();
        ImageProcessor segmentedLabeledPointsImageProc = segmentedLabeledPointsImage.getProcessor();
        
        
        ImagePlus segmentedComponentLabeledPointsImage = new ImagePlus(this.imageName + title, this.modell.getImageStack());
        ImageProcessor segmentedComponentLabeledPointsImageProc = segmentedComponentLabeledPointsImage.getProcessor();
        
        for(Voxel v : this.segmentedPoints){
        	segmentedLabeledPointsImage.setSlice(v.getZ());
        	segmentedPointsImage.setSlice(v.getZ());
        	segmentedLabeledPointsImageProc.putPixel(v.getX(), v.getY(), calcLabel(v.getValue()));
        	segmentedPointsImageProc.putPixel(v.getX(), v.getY(), v.getValue());
        }
        
        for(Voxel v : this.ventriclePoints) {
        	segmentedComponentLabeledPointsImage.setSlice(v.getZ());
        	segmentedComponentLabeledPointsImageProc.putPixel(v.getX(), v.getY(), v.getValue());
        }
        
		/*
		 * Create postfix number
		 */
		if(this.seqCounter <= 9){
			counter = "00" + this.seqCounter;
		}
		else if(this.seqCounter > 9 && this.seqCounter <= 99){
			counter = "0" + this.seqCounter;
		}
		else{
			counter = "" + this.seqCounter;
		}
		
		segmentedPointsImage.setTitle(segmentedPointsImage.getTitle() + " [" + counter + "]");
		segmentedPointsImage.setSlice(1);
		
		segmentedLabeledPointsImage.setTitle(segmentedLabeledPointsImage.getTitle() + " [" + counter + "]");
		segmentedLabeledPointsImage.setSlice(1);
		
		segmentedComponentLabeledPointsImage.setTitle(segmentedComponentLabeledPointsImage.getTitle() + " [" + counter + "]");
		segmentedComponentLabeledPointsImage.setSlice(1);
		
		// The image with Connected component
		segmentedComponentLabeledPointsImage.show();
		
		// The segmented Points only
//		segmentedPointsImage.show();
		
		//The image with the sgemented points + the whole image
		segmentedLabeledPointsImage.show();
	}
	
	
	/**
	 * Calculates a transparent label to mark the segmented points.
	 * @param 	pixel
	 * 				Original pixel value
	 * @return	Point label
	 */
	private int[] calcLabel(int pixel){
		int alpha = (int)(255 / 2d);
		int red = Modell.INTERNAL_POINTS_LABEL.getRed();
		int green = Modell.INTERNAL_POINTS_LABEL.getGreen();
		int blue = Modell.INTERNAL_POINTS_LABEL.getBlue();
		
		int[] rgbPixel = {
				((alpha * pixel + (255 - alpha) * red) / 255),
				((alpha * pixel + (255 - alpha) * green) / 255),
				((alpha * pixel + (255 - alpha) * blue) / 255)};
		return rgbPixel;
	}
	
	/**
	 * Get all labeled Voxels into an ArrayList.
	 * 
	 */
	private void getSegmentationResult(){
		this.sd.setTime("");
		int coordinate1D;
		int intColor = Modell.INTERNAL_POINTS_LABEL.getRGB();
		this.segmentedPoints = new ArrayList<Voxel>();
		int labeled = 0;
		this.connectedComponentsLabel = new int[this.width * this.height* this.size];
		this.ventriclePoints = new ArrayList<Voxel>();
		
		// Initialize the Array
		for ( int i = 0 ; i < this.connectedComponentsLabel.length ; i++) {
			connectedComponentsLabel[i] = 0;
		}
		
		for(int z = this.minRoiZ; z < this.maxRoiZ; z++){
			this.sd.setStatus("Calculating Seg. Result done: " + (int)(100 * (z - this.minRoiZ) / (this.maxRoiZ - this.minRoiZ)) + " %");
			for (int y = this.minRoiY; y < this.maxRoiY; y++){
				for (int x = this.minRoiX; x < this.maxRoiX; x++){
					coordinate1D = (int)(this.width * (this.height * z + y) + x);
					if (this.label[coordinate1D] == intColor){
						this.segmentedPoints.add(new Voxel(x, y, (z + 1), this.greyValue[coordinate1D]));
						addResult("" + x + "|" + y + "|" + z + "|" + this.greyValue[coordinate1D] + "|" + this.strength[coordinate1D]);
						
						//Connected Component Labeling
						if (this.label[coordinate1D] == intColor && this.connectedComponentsLabel[coordinate1D] == 0) {
							labeled++;
							if ( neighborOption == CASegmentation3D.NEIGHBOR_MOORE) {
								
								if (!observeMoore(x,y,z,labeled,intColor)) {
									labeled--;
								}
							}
							if ( neighborOption == CASegmentation3D.NEIGHBOR_VON_NEUMANN) {
								if (!observeVonNeumann(x,y,z,labeled,intColor)) {
									labeled--;
								}
							}
						}
					}
				}
			}
		}
		
		//Writing into the Array
		for(int z = this.minRoiZ; z < this.maxRoiZ; z++){
			this.sd.setStatus("Calculating Connected component done: " + (int)(100 * (z - this.minRoiZ) / (this.maxRoiZ - this.minRoiZ)) + " %");
			for (int y = this.minRoiY; y < this.maxRoiY; y++){
				for (int x = this.minRoiX; x < this.maxRoiX; x++){
					coordinate1D = (int)(this.width * (this.height * z + y) + x);
					if (connectedComponentsLabel[coordinate1D] == 1 ) {
						this.ventriclePoints.add(new Voxel(x,y,( z + 1 ),Modell.VENTRICLE_A_POINTS_LABEL.getRGB()));
					}
					if (connectedComponentsLabel[coordinate1D] == 2) {
						this.ventriclePoints.add(new Voxel(x,y,( z + 1 ),Modell.VENTRICLE_B_POINTS_LABEL.getRGB()));
					}
				}
			}
		}
	}
	
	/**
	 * Connected Component Labeling with Moore neighboorhood
	 * 
	 * @param x
	 * @param y
	 * @param z
	 * @param label
	 * @param intColor
	 * @return 
	 * 			true: everything is fine, its one huge label
	 * 			false: its an outlier
	 */
	private boolean observeMoore(int x, int y, int z, int label,int intColor) {
		
		int voxelCtr = 0;
		ArrayList<Integer> coordList = new ArrayList<Integer>();
		
		int coordinate1D;
		Stack<Voxel> imageStack = new Stack<Voxel>();
		Voxel tmpVoxel;
		
		imageStack.push(new Voxel(x,y,z,0));
		
		while (!imageStack.isEmpty()) {
		
			tmpVoxel = imageStack.pop();
			voxelCtr++;
			
			coordinate1D = (int)(this.width * (this.height * tmpVoxel.getZ() 
					+ tmpVoxel.getY() ) + tmpVoxel.getX());
			coordList.add(coordinate1D);
			
			if (this.label[coordinate1D] == intColor && this.connectedComponentsLabel[coordinate1D] == 0) {
				this.connectedComponentsLabel[coordinate1D] = label;
				
				//x-Layer 
				if ( tmpVoxel.getX() + 1 < this.width) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1 , tmpVoxel.getY(), tmpVoxel.getZ() , 0));
				}
				if ( tmpVoxel.getX() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1 , tmpVoxel.getY(), tmpVoxel.getZ() , 0));
				}
				//y-Layer
				if ( tmpVoxel.getY() + 1 < this.height) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() + 1 , tmpVoxel.getZ() , 0));
				}
				if ( tmpVoxel.getY() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() - 1 , tmpVoxel.getZ() , 0));
				}
				//z-Layer
				if ( tmpVoxel.getZ() + 1 < this.size) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() , tmpVoxel.getZ() + 1, 0));
				}
				if ( tmpVoxel.getZ() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() , tmpVoxel.getZ() - 1, 0));
				}
				
				//x-y-Layer
				if ( tmpVoxel.getX() + 1 < this.width && tmpVoxel.getY() + 1 < this.height) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1 , tmpVoxel.getY() + 1 , tmpVoxel.getZ(), 0));
				}
				if ( tmpVoxel.getX() + 1 < this.width && tmpVoxel.getY() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1 , tmpVoxel.getY() - 1 , tmpVoxel.getZ(), 0));
				}
				if ( tmpVoxel.getX() - 1  > 0 && tmpVoxel.getY() + 1 < this.height) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1 , tmpVoxel.getY() + 1 , tmpVoxel.getZ(), 0));
				}
				if ( tmpVoxel.getX() - 1 > 0 && tmpVoxel.getY() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1 , tmpVoxel.getY() - 1 , tmpVoxel.getZ(), 0));
				}
				//x-z-Layer
				if (tmpVoxel.getX() + 1 < this.width && tmpVoxel.getZ() + 1 < this.size) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1 , tmpVoxel.getY() , tmpVoxel.getZ() + 1, 0));
				}
				if (tmpVoxel.getX()  + 1 < this.width && tmpVoxel.getZ() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1 , tmpVoxel.getY() , tmpVoxel.getZ() - 1, 0));
				}
				if (tmpVoxel.getX() - 1 > 0 && tmpVoxel.getZ() + 1 < this.size) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1 , tmpVoxel.getY() , tmpVoxel.getZ() + 1, 0));
				}
				if (tmpVoxel.getX() - 1 > 0 && tmpVoxel.getZ() - 1> 0 ) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1 , tmpVoxel.getY() , tmpVoxel.getZ() - 1, 0));
				}
				//y-z-Layer
				if (tmpVoxel.getY() + 1 < this.height && tmpVoxel.getZ() + 1 < this.size) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() + 1 , tmpVoxel.getZ() + 1, 0));
				}
				if (tmpVoxel.getY() + 1 < this.height && tmpVoxel.getZ() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() + 1 , tmpVoxel.getZ() - 1, 0));
				}
				if (tmpVoxel.getY() - 1 > 0 && tmpVoxel.getZ() + 1 < this.size) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() - 1 , tmpVoxel.getZ() + 1, 0));
				}
				if (tmpVoxel.getY() -1 > 0 && tmpVoxel.getZ() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() - 1 , tmpVoxel.getZ() - 1, 0));
				}
				//x-y-z-Layer
				if (tmpVoxel.getX() + 1 < this.width && tmpVoxel.getY() + 1 < this.height && tmpVoxel.getZ() + 1 < this.size) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1, tmpVoxel.getY() + 1 , tmpVoxel.getZ() + 1, 0));
				}
				if (tmpVoxel.getX() + 1 < this.width && tmpVoxel.getY() + 1 < this.height && tmpVoxel.getZ() -1 > 0 ) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1, tmpVoxel.getY() + 1 , tmpVoxel.getZ() - 1, 0));
				}
				if (tmpVoxel.getX() + 1 < this.width && tmpVoxel.getY() - 1 > 0 && tmpVoxel.getZ() + 1 < this.size ) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1, tmpVoxel.getY() - 1 , tmpVoxel.getZ() + 1, 0));
				}
				if (tmpVoxel.getX() + 1 < this.width && tmpVoxel.getY() -1  > 0 && tmpVoxel.getZ() - 1  > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1, tmpVoxel.getY() - 1 , tmpVoxel.getZ() - 1, 0));
				}
				if (tmpVoxel.getX() -1  > 0 && tmpVoxel.getY() + 1 < this.height && tmpVoxel.getZ() + 1 < this.size) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1, tmpVoxel.getY() + 1 , tmpVoxel.getZ() + 1, 0));
				}
				if (tmpVoxel.getX() - 1 > 0 && tmpVoxel.getY() + 1 < this.height && tmpVoxel.getZ() - 1 > 0 ) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1, tmpVoxel.getY() + 1 , tmpVoxel.getZ() - 1, 0));
				}
				if (tmpVoxel.getX() - 1 > 0 && tmpVoxel.getY() - 1 > 0 && tmpVoxel.getZ() + 1 < this.size ) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1, tmpVoxel.getY() - 1 , tmpVoxel.getZ() + 1, 0));
				}
				if (tmpVoxel.getX() - 1 > 0 && tmpVoxel.getY() - 1 > 0 && tmpVoxel.getZ() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1, tmpVoxel.getY() - 1 , tmpVoxel.getZ() - 1, 0));
				}
			}
		}
		
		if ( voxelCtr < maxOutlier) {
			
			//Mark as islands
			for (Integer i : coordList ) {
				this.connectedComponentsLabel[i.intValue()] = -1;
			}
			coordList = null;
			System.gc();
			return false;
		} else {
			coordList = null;
			System.gc();
			return true;
		}
		
	}
	

	/**
	 * Connected Component Labeling with von Neumann Neighborhood
	 * 
	 * @param x
	 * @param y
	 * @param z
	 * @param label
	 * @param intColor
	 * @return 
	 * 			true: everything is fine, its one huge label
	 * 			false: its an outlier
	 */
	private boolean observeVonNeumann(int x, int y , int z, int label, int intColor) {
		
		int voxelCtr = 0;
		ArrayList<Integer> coordList = new ArrayList<Integer>();
		
		int coordinate1D;
		Stack<Voxel> imageStack = new Stack<Voxel>();
		Voxel tmpVoxel;
		
		imageStack.push(new Voxel(x,y,z,0));
		
		while (!imageStack.isEmpty()) {
		
			tmpVoxel = imageStack.pop();
			voxelCtr++;
			
			coordinate1D = (int)(this.width * (this.height * tmpVoxel.getZ() + tmpVoxel.getY()) + tmpVoxel.getX());
			coordList.add(coordinate1D);
			
			if (this.label[coordinate1D] == intColor && this.connectedComponentsLabel[coordinate1D] == 0) {
				this.connectedComponentsLabel[coordinate1D] = label;
				
				//x-Layer 
				if ( tmpVoxel.getX() + 1 < this.width) {
					imageStack.push(new Voxel(tmpVoxel.getX() + 1 , tmpVoxel.getY(), tmpVoxel.getZ() , 0));
				}
				if ( tmpVoxel.getX() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() - 1 , tmpVoxel.getY(), tmpVoxel.getZ() , 0));
				}
				//y-Layer
				if ( tmpVoxel.getY() + 1 < this.height) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() + 1 , tmpVoxel.getZ() , 0));
				}
				if ( tmpVoxel.getY() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() - 1 , tmpVoxel.getZ() , 0));
				}
				//z-Layer
				if ( tmpVoxel.getZ() + 1 < this.size) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() , tmpVoxel.getZ() + 1, 0));
				}
				if ( tmpVoxel.getZ() - 1 > 0) {
					imageStack.push(new Voxel(tmpVoxel.getX() , tmpVoxel.getY() , tmpVoxel.getZ() - 1, 0));
				}
			}
		}

		if ( voxelCtr < maxOutlier) {
			
			//Mark as islands
			for (Integer i : coordList ) {
				this.connectedComponentsLabel[i.intValue()] = -1;
			}
			coordList = null;
			System.gc();
			return false;
			
		} else {
			coordList = null;
			System.gc();
			return true;
		}
	}
	
	
	/**
	 * Calculates the remaining segmentation time after the first step.
	 * 
	 * @param 	start
	 * 				Segmentation start time
	 */
	private void calcTimeLeft(long start){
		long now = System.currentTimeMillis();
		this.timeLeft = (this.steps - 1) * (now - start);
	}
	
	/**
	 * Depending on the choosen neighbor-options this methode calls
	 * the segmentation function {@link performSegmentation()} in different ways.
	 * 
	 * @param 	x
	 *				X-Coordinate which must be segmented
	 * @param 	y
	 *            	Y-Coordinate which must be segmented
	 * @param 	z
	 *            	Z-Coordinate which must be segmented
	 */
	private void performSegmentationCellStep(int x, int y, int z){
		int coordinate1D = (int)(this.width * (this.height * z + y) + x);	
	
		this.labelT1[coordinate1D] = this.label[coordinate1D];
		this.strengthT1[coordinate1D] = this.strength[coordinate1D];
	
		/*
		 * Moore neighborhood
		 */
		if(this.neighborOption == CASegmentation3D.NEIGHBOR_MOORE){
			for(int zz = -1; zz <= 1; zz++){
				for(int yy = -1; yy <= 1; yy++){
					for(int xx = -1; xx <= 1; xx++){
						performSegmentation(x, y, z, xx, yy, zz, coordinate1D);
					}
				}
			}
		}
		
		/*
		 * von Neumann neighborhood
		 */
		if(this.neighborOption == CASegmentation3D.NEIGHBOR_VON_NEUMANN){
			for(int zz = -1; zz <= 1; zz++){
				performSegmentation(x, y, z, 0, 0, zz, coordinate1D);
			}
			for(int yy = -1; yy <= 1; yy++){
				performSegmentation(x, y, z, 0, yy, 0, coordinate1D);
			}
			for(int xx = -1; xx <= 1; xx++){
				performSegmentation(x, y, z, xx, 0, 0, coordinate1D);
			}
		}
	}
	
	/**
	 * Performs a segmentation step for one single cell and its adjacenting
	 * cells depending on the choosen neighborhood.
	 * 
	 * @param 	x
	 *				X-Coordinate which must be segmented
	 * @param 	y
	 *            	Y-Coordinate which must be segmented
	 * @param 	z
	 *            	Z-Coordinate which must be segmented
	 * @param xx
	 * 				Difference on X-Axis between current cell and its current neighbor
	 * @param yy
	 * 				Difference on Y-Axis between current cell and its current neighbor
	 * @param zz
	 * 				Difference on Z-Axis between current cell and its current neighbor
	 * @param coordinate1D
	 * 				1D-Coordinate of current cell
	 */
	private void performSegmentation(int x, int y, int z, int xx, int yy, int zz, int coordinate1D){
		double estimator = 0.0d;
		
		if ( (zz + z) < this.minRoiZ || ( zz + z ) >= this.maxRoiZ ) {
			return;
		}
		
		int neighCoordinate1D = (int)(this.width * (this.height * (z + zz) + (y + yy)) + (x + xx));
		
		if (neighCoordinate1D < 0)
			return;
		if (neighCoordinate1D >= (this.height * this.width * this.size ))
			return;
		
		/*
		 * Local grey value
		 */
		if(this.greyValueOption == CASegmentation3D.MAXGREYVALUE_LOCAL){
			for(int zzz = -1; zzz <= 1; zzz++){
				for(int yyy = -1; yyy <= 1; yyy++){
					for(int xxx = -1; xxx <= 1; xxx++){
						
						if ( ( z + zzz ) < this.minRoiZ || ( z + zzz ) >= this.maxRoiZ ) {
							continue;
						}
						
						int greyCoordinate1D = (int)(this.width * (this.height * (z + zzz) + (y + yyy)) + (x + xxx));
						if(greyCoordinate1D < 0)
							continue;
						if(greyCoordinate1D >= (this.height * this.width * this.size))
							continue;
						if(this.greyValue[greyCoordinate1D] > this.maxGreyValue){
							this.maxGreyValue = this.greyValue[greyCoordinate1D];
						}
					}
				}
			}
		}
		
		/*
		 * Calculating grey vector between current point and neighbor point
		 */
		int grayVector = this.greyValue[coordinate1D] - this.greyValue[neighCoordinate1D];

		/*
		 * Euclidean distance
		 */
		if(this.distanceOption == CASegmentation3D.DISTANCE_EUCLIDEAN){
			estimator = segmentationFunction(euclideanDistance(grayVector, grayVector, grayVector), 
					euclideanDistance(this.maxGreyValue, this.maxGreyValue, this.maxGreyValue)) 
					* this.strength[neighCoordinate1D];
		}
		
		/*
		 * Manhattan distance
		 */
		if(this.distanceOption == CASegmentation3D.DISTANCE_MANHATTAN){
			estimator = segmentationFunction(manhattanDistance(grayVector, grayVector, grayVector), 
					manhattanDistance(this.maxGreyValue, this.maxGreyValue, this.maxGreyValue))
					* this.strength[neighCoordinate1D];
		}
		
		/*
		 * Strength between slices
		 */
		if(zz != 0 && this.label[coordinate1D] == Modell.INTERNAL_POINTS_LABEL.getRGB()){
			estimator *= this.strengthFactor;
		}
		
		if(estimator > this.strength[coordinate1D]){
			this.labelT1[coordinate1D] = this.label[neighCoordinate1D];
			this.strengthT1[coordinate1D] = estimator;
		}
	}
	
	/**
	 * The simple segmentation estimation Function. Computes the probability
	 * that one cell takes over the neighbor cell.
	 * 
	 * @param	x
	 * 				The computed euclidean distance norm
	 * @param 	maxValue
	 *            	Is 255^3 e-norm in a gray value
	 * @return 	Probability between 0 and 1 (exluded)
	 */
	private double segmentationFunction(double x, double maxValue){
		return 1.0d - (x / maxValue);
	}
	
	/**
	 * Computes the Euclidean Distance of the Gray Level vector represented as
	 * RGB value.
	 * 
	 * @param 	v1
	 * 				Red value
	 * @param 	v2
	 * 				Green value
	 * @param 	v3
	 * 				Blue value
	 * @return 	Euclidean Distance from v1, v2, v3
	 */
	private double euclideanDistance(int v1, int v2, int v3){
		return Math.sqrt(v1 * v1 + v2 * v2 + v3 * v3);
	}


	/**
	 * Computes the Manhattan Distance of the Gray Level vector represented as
	 * RGB value.
	 * 
	 * @param 	v1
	 * 				Red value
	 * @param 	v2
	 * 				Green value
	 * @param 	v3
	 * 				Blue value
	 * @return 	Manhattan Distance from v1, v2, v3
	 */
	private double manhattanDistance(int v1, int v2, int v3){
		return (Math.sqrt(v1 * v1) + Math.sqrt(v2 * v2) + Math.sqrt(v3 * v3));
	}
	
	/**
	 * Initialization to perform next step from ti -> ti+1
	 */
	private void prepareNextStep(){
		for(int z = 0; z < this.size; z++){
			for(int y = 0; y < this.height; y++){
				for(int x = 0; x < this.width; x++){
					int coordinate1D = (int)(this.width * (this.height * z + y) + x);
					this.label[coordinate1D] = this.labelT1[coordinate1D];
					this.strength[coordinate1D] = this.strengthT1[coordinate1D];
				}
			}
		}
	}
	
	/**
	 * Writes the segmentation result to a simple .txt file.
	 * 
	 * @param 	resultText
	 * 				Complete result text
	 */
	private void writeResult(String resultText){
		this.sd.setStatus("Writing Result File...");
		BufferedWriter bw;
		TimeZone.setDefault(TimeZone.getTimeZone(System.getProperty("user.timezone")));
		this.sdf = new SimpleDateFormat("HH_mm_ss - dd.MM.yyyy");
		try{
			bw = new BufferedWriter(new FileWriter("" + this.imageName + " - " + this.sdf.format(this.segStop) + ".txt"));
			bw.write(resultText);
			bw.close();
		}catch(IOException ioe){
			System.out.println("Fehler beim Schreiben der Datei!");
		}
	}


	/**
	 * Clears all memory expensive variables and calls the GC.
	 */
	private void clearAll(){
		this.greyValue = null;
		this.label = null;
		this.labelT1 = null;
		this.strength = null;
		this.strengthT1 = null;
		this.sd = null;
	//	this.segmentedPoints = null;
		this.ventriclePoints = null;
		this.connectedComponentsLabel = null;
		
		this.modell = null;
		
		IJ.freeMemory();
		System.gc();
	}

	
	
	/**
	 * Updates the Status Dialog-Timer
	 */
	public void updateSD(){
		this.timeLeft -= 1000;
		if(this.sdfSD == null){
			TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
			this.sdfSD = new SimpleDateFormat("HH:mm:ss");
		}
		
		if(this.timeLeft <= 999 && this.timer != null){
			this.timer.cancel();
			this.timer = null;
			System.gc();
			return;
		}
		else{
			this.sd.setTime("Time left: " + this.sdfSD.format(this.timeLeft));
		}
	}
	
	/**
	 * Creates a new image for saving the segmentation result
	 * 
	 * @return 	binary image
	 */
	public ImagePlus createBinaryImageSequence(){
		ImagePlus binaryImage;
		
    	binaryImage = NewImage.createByteImage(this.imageName + " Binary Seg. Result", 
    			this.width, this.height, this.size, NewImage.FILL_WHITE);


		ImageProcessor binaryImageProc = binaryImage.getProcessor();
		
        for(Voxel v : this.segmentedPoints){
        	binaryImage.setSlice(v.getZ());
        	binaryImageProc.putPixel(v.getX(), v.getY(), 0);
        }
        
        return binaryImage;
	}
	
	/**
	 * Reads the dimension-Parameters from the model and sets them accordingly
	 */
	private void getDimensionParameters() {
		this.width = this.modell.getWidth();
		this.height = this.modell.getHeight();
		this.size = this.modell.getSize();
		this.minRoiX = this.modell.getMinRoiX();
		this.maxRoiX = this.modell.getMaxRoiX();
		this.minRoiY = this.modell.getMinRoiY();
		this.maxRoiY = this.modell.getMaxRoiY();
		this.minRoiZ = this.modell.getMinRoiZ();
		this.maxRoiZ = this.modell.getMaxRoiZ();
	}
	
	// //////////////////////
	// Setter & Getters ////
	// //////////////////////

	/**
	 * Set Current Volume /GreyValue
	 */
	public void setGreyValue(int[] greyValue) {
		this.greyValue = greyValue;
	}


	/**
	 * Set the steps after which the current status of the segmentation is
	 * visualized.
	 * 
	 * @param tmpSteps
	 */
	public void setTmpSteps(int tmpSteps) {
		this.tmpSteps = tmpSteps;
	}

	/**
	 * Set segmentation step factor
	 * 
	 * @param stepsFactor
	 */
	public void setStepsFactor(int stepsFactor) {
		this.stepsFactor = stepsFactor;
	}


	/**
	 * Set Neighbor option
	 * 
	 * @param neighborOption
	 *            Neighbor option
	 */
	public void setNeighborOption(int neighborOption) {
		this.neighborOption = neighborOption;
	}

	/**
	 * Set Greyvalue option
	 * 
	 * @param greyValueOption
	 *            GreyValue option
	 */
	public void setGreyValueOption(int greyValueOption) {
		this.greyValueOption = greyValueOption;
	}

	/**
	 * Set Distance option
	 * 
	 * @param distanceOption
	 *            Distance option
	 */
	public void setDistanceOption(int distanceOption) {
		this.distanceOption = distanceOption;
	}

	/**
	 * Set Sequence Counter
	 * 
	 * @param seqCounter
	 *            Sequence Counter
	 */
	public void setSeqCounter(int seqCounter) {
		this.seqCounter = seqCounter;
	}

	/**
	 * Set strength factor
	 * 
	 * @param strengthFactor
	 *            Strength factor
	 */
	public void setStrengthFactor(double strengthFactor) {
		this.strengthFactor = strengthFactor;
	}

	/**
	 * Set Image name
	 * 
	 * @param imageName
	 *            Image name
	 */
	public void setImageName(String imageName) {
		this.imageName = imageName;
	}

	/**
	 * Indicates if the Segmentation is still running
	 * 
	 * @return true - still running <br>
	 *         false - not running
	 */
	public boolean isRunning() {
		return running;
	}

	// /////////////////////////////////////////////

	
	// ActionListener
	@Override
	@SuppressWarnings("deprecation")
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == this.sd.getBtnAbort()){
			synchronized (this.segThread){
				if(this.timer != null){
					this.timer.cancel();
					this.timer = null;
				}
				System.gc();
				this.segThread.stop();
			}
			this.running = false;
			this.sd.close();	
		}
	}

	// ///////////////////////////////////////////////

	// Windowlistener
	@Override
	public void windowActivated(WindowEvent e) {
		//empty block
	}

	@Override
	public void windowClosed(WindowEvent e) {
		//empty block
	}

	@Override
	@SuppressWarnings("deprecation")
	public void windowClosing(WindowEvent e) {
		if(e.getSource() == this.sd.getBtnAbort()){
			synchronized (this.segThread){
				if(this.timer != null){
					this.timer.cancel();
					this.timer = null;
				}
				System.gc();
				this.segThread.stop();
			}
			this.running = false;
			this.sd.close();	
		}
	}

	@Override
	public void windowDeactivated(WindowEvent e) {
		//empty block
	}

	@Override
	public void windowDeiconified(WindowEvent e) {
		//empty block
	}

	@Override
	public void windowIconified(WindowEvent e) {
		//empty block
	}

	@Override
	public void windowOpened(WindowEvent e) {
		//empty block
	}
	
	
	/**
	*
	* Bachelor thesis at the Institute of Biomedical Engineering, 
	* located at the UMIT/EWZ campus in Hall in Tyrol. <br><br>
	* 
	* This class just generate a new {@link TimerUpdatesender} object for 
	* updating the segmentation status dialog.
	* 
	* @author Michael Baumann
	*
	*/
	private class TimerUpdate extends TimerTask{
		
		/** CASegmentation3D instance. */
		private CASegmentation3D instance;
		
		
		/**
		 * TimerUpdate constructor.
		 * 
		 * @param 	instance
		 * 				CASegmentation3D object
		 */
		public TimerUpdate(CASegmentation3D instance){
			this.instance = instance;
		}
		
		
		/**
		 * Reimplemented TimerTask-run method.
		 */
		public void run(){
			this.instance.updateSD();
		}
	}
}

